package anonymouschat.client.network;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.swing.JOptionPane;

import anonymouschat.client.Client;
import anonymouschat.client.PublicIdentity;
import anonymouschat.messages.EncryptedMessageWrapperMessage;
import anonymouschat.messages.KeyResponse;
import anonymouschat.messages.securitymessages.RSAEncryptedMessage;
import anonymouschat.utils.Constants;
import anonymouschat.utils.LocalFileOps;
import anonymouschat.utils.Print;

/**
 * This thread receives incoming messages.
 * 
 * @author bh349
 * 
 */
public class ClientInConnectionThread extends Thread
{
    // Socket connected to the initial server for the clients route.
    private Socket incomingSocket;

    // Client this tread is processing incoming data for
    private Client client;

    ObjectInputStream in = null;

    ObjectOutputStream out = null; // needed to write the initial
                                   // AnnoinceMessage to server

    public ClientInConnectionThread( Client client, Socket clientSocket ) {
        this.client = client;
        this.incomingSocket = clientSocket;
    }

    /**
     * Wait for out to be created and then return it. This method exists because
     * we can't use a socket's output stream to create a object output stream
     * more than once 
     * 
     * @return
     * @throws Exception 
     */
    public ObjectOutputStream getOutputStream() throws Exception {
    	int i=0;
        while( this.out == null ) {
            try {
            	if (i > 500)
            		throw new Exception("Could not establish output connection to initial server");
                Thread.sleep( 20 );
                i++;
            } catch( InterruptedException e ) {
                Print.error( "Error sleeping", e );
            }
        }
        return out;
    }

    @Override
    public void run() {
		try {
			out = new ObjectOutputStream(incomingSocket.getOutputStream());
		} catch (IOException e1) {
			Print.error("Error creating ObjectOutputStream", e1);
		}
		Object receivedMessage = null;
		boolean openSocket = true;
		try {
			in = new ObjectInputStream(incomingSocket.getInputStream());
		} catch (IOException e1) {
			Print.error("Error creating ObjectInputStream", e1);
		}
		while (openSocket == true) {
			try {
				receivedMessage = in.readObject();
				Print.debug( "Received a new message: " + receivedMessage.toString() );
				if (receivedMessage instanceof EncryptedMessageWrapperMessage) {
					EncryptedMessageWrapperMessage encMsg = (EncryptedMessageWrapperMessage) receivedMessage;
					Object obj = null;
					for (int i = 0; i < Client.serverChain.size(); i++) {
					    Print.debug( "Removing layer of encryption: " + i );
						Cipher dec = Cipher
								.getInstance(Constants.SYMMETRIC_CIPHER_TYPE);
						dec.init(Cipher.DECRYPT_MODE, Client.serverChain.get(i)
								.getSymmetricKey(), encMsg.getIV());
						ByteArrayInputStream bis = new ByteArrayInputStream(
								dec.doFinal(encMsg.getencryptedMessage()));
						ObjectInputStream is = new ObjectInputStream(bis);
						obj = is.readObject();
						if (obj instanceof EncryptedMessageWrapperMessage) {
							encMsg = (EncryptedMessageWrapperMessage) obj;
						} else if(obj instanceof KeyResponse) {
						    break; //done unwrapping
						} else if(obj instanceof RSAEncryptedMessage){
						    break; //done unwrapping
						} else {
						    LocalFileOps.appendAuditFile("When decrypting EncryptedMessageWrapperMessage, got unexpected type of object"
									+ obj.toString());
							Print.error("When decrypting EncryptedMessageWrapperMessage, got unexpected type of object"
									+ obj.toString());
						}
					}
					if (obj instanceof KeyResponse) {
						// message type is key response
						KeyResponse pkResp = (KeyResponse) obj;
						if (pkResp.getPublicIdentity() == null) {
							// target identity not found.
							client.getPublicIdentityMap().put(
									pkResp.getUserName(), null);
						} else {
							String userNameString = PublicIdentity
									.composeUserName(pkResp.getPublicIdentity()
											.getPublicRSAKey(), pkResp
											.getPublicIdentity().getVerKey());
							if (userNameString.equals(pkResp.getUserName())) {
								// correctly verified username.
								Print.debug("correctly verified username, get the publicIdentity of "+ pkResp.getUserName());
								client.getPublicIdentityMap().put(
										pkResp.getUserName(),
										pkResp.getPublicIdentity());
							} else {
								// user name doesn't match the identity.
								Print.error("The key response contains bad identity");
								client.getPublicIdentityMap().put(
										pkResp.getUserName(), null);
							}
						}
					} else if (obj instanceof RSAEncryptedMessage) {
						//received encrypted text message 
						Print.debug("RSAEncryptedMessage:");
						Print.debug(new String(((RSAEncryptedMessage) obj).getEncryptedData()));
						client.handleRSAEncryptedMessage((RSAEncryptedMessage) obj);
					}
				}
			} catch (ClassNotFoundException e) {
				Print.error("Error reading an object", e, null);
			} catch (IOException ioe) {
				Print.error("IOError reading an object");
				openSocket = false; // Socket Closed, die gracefully
				JOptionPane.showMessageDialog( null,
	                    "Your initialization server died. I am sorry but byebye...", "Error",
	                    JOptionPane.ERROR_MESSAGE );
				System.exit(0);
			} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
			} catch (NoSuchPaddingException e) {
				e.printStackTrace();
			} catch (InvalidKeyException e) {
				e.printStackTrace();
			} catch (IllegalBlockSizeException e) {
				e.printStackTrace();
			} catch (BadPaddingException e) {
				e.printStackTrace();
			} catch (InvalidAlgorithmParameterException e) {
				e.printStackTrace();
			}
		}// END-WHILE
		try {
			if (in != null)
				in.close();
			if (incomingSocket != null)
				incomingSocket.close();
		} catch (IOException e) {
			Print.error("Error closing sockets", e);
		}
	}
}